package com.csaba.connector.bha.com;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URL;
import java.security.KeyException;
import java.security.SecureRandom;
import java.util.logging.Logger;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;

import com.csaba.connector.web.util.VerisignCertificate;
import com.csaba.util.LoggerHelper;
import com.ind.simplenode.SimpleNode;

/**
 * This class handles communication to the bank.
 * Referencing classes in the Original applet:
 * - MainApplet holds the main reference to the single SecureCommunication object. It is also
 * 		responsible for handling timeout, and logout.
 * - Login: initialize this object according to the demo settings.
 * 
 * - Context handles request life-cycle. All request is indicated through the Context object.
 *
 */
public class SecureCommunication
{
	private final static Logger logger = LoggerHelper.getDefaultLogger();

	private boolean keyExchangeNeeded = true;
	private final byte randomSeed[];
	private final byte blowfishKey[];
	private Blowfish encrypter;
	private String sessionIdCookie;
	private String sessionId;

	private static BigInteger rsaKey1 = new BigInteger(
			"21048735217126933739151021757828383542708831577015665921662993222080389262331323945206936668286336666230453922688029881117102622065506604401702782431208325822921494323470887129767957530377101233062092900458820355770812908527353431509357097683245058066053850128612141480174494898743432494273088806968370767546098228902339229208148280044891590836210527995954164172502563532769692991139910588667681069732619482857671907584336724287148511900250919075946277438760328321558198267824700235429042677995585364087056510998442126402348540245296921631466165256166163979193828145266362182479488540208254619882175338610996421762491");
	private static BigInteger rsaKey2 = new BigInteger(
			"19957801062873049774635014414924872090057221028533892378963697588360591937810274802898465186277584181644570676233611492264672906868037774150510175430875057187639688189208530999316106407830097535023568021987584844314196364993685478368621637573957539873357587921298372012194977715898826633442944121102619314777145584920078100696103885508096428977202411960567380835420597644298628847443059445262553721205716811223939293853208067817312437277233944887773111443634509537223378636161201805435353178032178522235937917361791118498195115329046622295148467230764712691494733968593932197660643341678553004756615861681073938658879");

	private final URL serverURL;

	private final static SSLContext sslContext;
	static
	{
		sslContext = VerisignCertificate.installCert();
	}

	/**
	 * @param randomSeed It is a 200 bytes of random data coming from the mouse events at initialization of the applet.
	 * @param serverURL URL of the server. This is received from the enclosing HTML. For example: https://ibank.bha.com/IBWBGateway/IBGatewayServlet
	 */
	public SecureCommunication( final byte randomSeed[], final URL serverURL )
	{
		blowfishKey = new byte[8];
		this.serverURL = serverURL;
		this.randomSeed = randomSeed;
	}

	private void exchangeKey() throws IOException, KeyException
	{
		logger.fine("key exchage started.");
		final SecureRandom random = new SecureRandom(randomSeed);
		do
			random.nextBytes(blowfishKey);
		while ( blowfishKey[0] <= 0 );

		final byte randomKey[] = new byte[56];
		random.nextBytes(randomKey);

		encrypter = new Blowfish(randomKey);

		final byte longKey[] = new byte[randomKey.length + blowfishKey.length];
		System.arraycopy(blowfishKey, 0, longKey, 0, blowfishKey.length);
		System.arraycopy(randomKey, 0, longKey, blowfishKey.length, randomKey.length);

		final BigInteger keyInt = new BigInteger(longKey);
		final BigInteger keyModPow = keyInt.modPow(rsaKey2, rsaKey1);
		final byte keyRequest[] = keyModPow.toByteArray();
		final byte encryptedReply[] = sendRequest(keyRequest, true);
		final byte reply[] = decrypt(encryptedReply);
		if ( !"OK".equals(new String(reply)) )
		{
			throw new IOException("Key exchange failed.");
		}
		else
		{
			logger.fine("BlowFish key successfully exchanged.");
			keyExchangeNeeded = false;
		}
	}

	public void clearSession()
	{
		keyExchangeNeeded = true;
	}

	public SimpleNode sendRequest( final SimpleNode request ) throws Exception
	{
		logger.fine(">>> " + request.getString("requestName"));

		if ( keyExchangeNeeded )
			exchangeKey();
		final byte requestBytes[] = SimpleNode.serialize(request);
		final byte encrzptedRequest[] = encrypt(requestBytes);
		final byte encryptedReply[] = sendRequest(encrzptedRequest, false);
		try
		{
			final byte replyBytes[] = decrypt(encryptedReply);
			final SimpleNode reply = SimpleNode.initialize(replyBytes);
			logger.fine("<<< " + request.getString("requestName"));
			return reply;
		}
		catch ( final Exception e )
		{
			logger.severe("Error reply: " + new String(encryptedReply));
			throw e;
		}
	}

	private byte[] sendRequest( final byte request[], final boolean newKeyRequest ) throws IOException
	{
		final HttpsURLConnection connect = (HttpsURLConnection) serverURL.openConnection();
		if ( sslContext != null )
		{
			connect.setSSLSocketFactory(sslContext.getSocketFactory());
		}
		connect.setDoOutput(true);
		if ( newKeyRequest )
		{
			connect.setRequestProperty("NewKeyRequest", "NEED");
			sessionIdCookie = null;
		}
		if ( sessionIdCookie != null )
			connect.setRequestProperty("Cookie", sessionIdCookie);

		//initialize output stream
		DataOutputStream dataoutputstream = null;
		if ( !newKeyRequest )
		{
			//add session ID
			final byte challange[] = ( "CSC515" + sessionId ).getBytes();
			final byte hash[] = MD5.getHashValue(challange);
			final byte hashedChallange[] = new byte[challange.length + hash.length];
			System.arraycopy(challange, 0, hashedChallange, 0, challange.length);
			System.arraycopy(hash, 0, hashedChallange, challange.length, hash.length);
			connect.setRequestProperty("Content-Length", String.valueOf(request.length + hashedChallange.length));
			dataoutputstream = new DataOutputStream(connect.getOutputStream());
			dataoutputstream.write(hashedChallange);
		}
		else
		{
			connect.setRequestProperty("Content-Length", String.valueOf(request.length));
			dataoutputstream = new DataOutputStream(connect.getOutputStream());
		}
		dataoutputstream.write(request);
		dataoutputstream.flush();
		dataoutputstream.close();
		final int responseCode = connect.getResponseCode();
		if ( responseCode != 200 )
			throw new IOException("Communication Error:" + responseCode + " (" + connect.getResponseMessage() + ")");
		final int length = connect.getContentLength();
		final byte replyBytes[] = new byte[length];
		final DataInputStream datainputstream = new DataInputStream(connect.getInputStream());
		datainputstream.readFully(replyBytes);

		//get session cookie
		String cookies = connect.getHeaderField("Set-Cookie");
		if ( cookies != null )
		{
			final int l = cookies.indexOf(";");
			cookies = cookies.substring(0, l < 0 ? cookies.length() : l);
			sessionIdCookie = cookies;
			final String fullSessionId = cookies.substring(cookies.indexOf("SESSIONID=") + "SESSIONID=".length(),
					cookies.indexOf(":"));
			sessionId = fullSessionId.substring(4);
			logger.fine("New session ID received: " + sessionId);
		}
		connect.getInputStream().close();
		return replyBytes;
	}

	public byte[] encrypt( final byte input[] )
	{
		final byte hash[] = MD5.getHashValue(input);
		final byte hashedInput[] = new byte[input.length + hash.length];
		System.arraycopy(input, 0, hashedInput, 0, input.length);
		System.arraycopy(hash, 0, hashedInput, input.length, hash.length);
		final byte encryptedInput[] = encrypter.encrypt(hashedInput, blowfishKey);
		return encryptedInput;
	}

	public byte[] decrypt( final byte encryptedInput[] )
	{
		final byte hashedInput[] = encrypter.decrypt(encryptedInput, blowfishKey);
		final byte input[] = new byte[hashedInput.length - 16];
		System.arraycopy(hashedInput, 0, input, 0, input.length);
		final byte localHash[] = MD5.getHashValue(input);

		//verify remote hash
		for ( int j = 0; j < localHash.length; j++ )
			if ( localHash[j] != hashedInput[( j + hashedInput.length ) - localHash.length] )
				throw new SecurityException("Invalid MD5 hash!");

		return input;
	}

	public String getSessionId()
	{
		return sessionId;
	}

}
